package gr.uoa.di.thanos.botcraft.gui;

import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.nio.FloatBuffer;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opencl.CLContext;
import org.lwjgl.opencl.CLDevice;
import org.lwjgl.opengl.AWTGLCanvas;
import org.lwjgl.util.Renderable;

import gr.uoa.di.thanos.botcraft.Configuration;
import gr.uoa.di.thanos.botcraft.game.Game;
import gr.uoa.di.thanos.botcraft.game.Map;
import gr.uoa.di.thanos.botcraft.game.Object;
import gr.uoa.di.thanos.botcraft.game.Team;
import gr.uoa.di.thanos.botcraft.game.Terrain;
import static org.lwjgl.opencl.CL10.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.*;
import static org.lwjgl.util.glu.GLU.*;

public class GuiClient extends JFrame implements Runnable {
	private class GameCanvas extends AWTGLCanvas implements ActionListener, KeyListener, MouseWheelListener, Renderable, WindowListener {
		private static final long serialVersionUID = 1L;
		
		private GameCanvas() throws LWJGLException {
			super();
//			super(configuration.getScreen(), new PixelFormat()); // 	TODO pixel format
//			setCLSharingProperties(properties); TODO share stuff between OpenCL and OpenGL
			addKeyListener(this);
			addMouseWheelListener(this);
		}
		
		@Override
		public void actionPerformed(final ActionEvent actionEvent) {
			if (getContext() != null) {
				try {
					makeCurrent();
					render();
					swapBuffers();
				} catch (final LWJGLException e) {
					LOGGER.log(Level.WARNING, "Error rendering canvas", e);
				}
			}
		}
		
		@Override
		public void componentResized(final ComponentEvent componentEvent) {
			if (getContext() != null) {
				try {
					makeCurrent();
					glViewport(0, 0, getWidth(), getHeight());
					glMatrixMode(GL_PROJECTION);
					glLoadIdentity();			
					gluPerspective(FIELD_OF_VIEW, getWidth() / ((getHeight() == 0) ? 1.0f : Integer.valueOf(getHeight()).floatValue()), Z_NEAR, Z_FAR);
				} catch (final LWJGLException e) {
					LOGGER.log(Level.WARNING, "Error resizing canvas", e);
				}
			}
		}
		
		@Override
		public void initGL() {
			setVSyncEnabled(true);
			setSwapInterval(1000 / configuration.getFrameRate());
			componentResized(null); // since initial window is created (and resized) before initializing OpenGL, call it another time here just before starting rendering
			glPolygonMode(GL_FRONT, GL_LINE);
			glCullFace(GL_BACK);
			glEnable(GL_CULL_FACE);
			glDepthFunc(GL_LEQUAL);
			glEnable(GL_DEPTH_TEST);
			
			glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
			glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
			glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

			glShadeModel(GL_SMOOTH);
			final FloatBuffer lightModelAmbientLight = BufferUtils.createFloatBuffer(4);
			lightModelAmbientLight.put(0, 1.0f);
			lightModelAmbientLight.put(1, 1.0f);
			lightModelAmbientLight.put(2, 1.0f);
			lightModelAmbientLight.put(3, 1.0f);
			glLightModel(GL_LIGHT_MODEL_AMBIENT, lightModelAmbientLight);
			glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
			glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
			final FloatBuffer light0AmbientLight = BufferUtils.createFloatBuffer(4);
			light0AmbientLight.put(0, 1.0f);
			light0AmbientLight.put(1, 1.0f);
			light0AmbientLight.put(2, 1.0f);
			light0AmbientLight.put(3, 1.0f);
			glLight(GL_LIGHT0, GL_AMBIENT, light0AmbientLight);
			final FloatBuffer light0DiffuseLight = BufferUtils.createFloatBuffer(4);
			light0DiffuseLight.put(0, 1.0f);
			light0DiffuseLight.put(1, 1.0f);
			light0DiffuseLight.put(2, 1.0f);
			light0DiffuseLight.put(3, 1.0f);
			glLight(GL_LIGHT0, GL_DIFFUSE, light0DiffuseLight);
			final FloatBuffer light0SpecularLight = BufferUtils.createFloatBuffer(4);
			light0SpecularLight.put(0, 1.0f);
			light0SpecularLight.put(1, 1.0f);
			light0SpecularLight.put(2, 1.0f);
			light0SpecularLight.put(3, 1.0f);
			glLight(GL_LIGHT0, GL_SPECULAR, light0SpecularLight);
			final FloatBuffer light0Position = BufferUtils.createFloatBuffer(4);
			light0AmbientLight.put(0, 0.0f);
			light0AmbientLight.put(1, -1.0f);
			light0AmbientLight.put(2, 0.0f);
			light0AmbientLight.put(3, 0.0f);
			glLight(GL_LIGHT0, GL_POSITION, light0Position);
			glEnable(GL_LIGHT0);
//			glEnable(GL_LIGHTING);

//			gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MAG_FILTER, GL2.GL_LINEAR);
//			gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_MIN_FILTER, GL2.GL_LINEAR_MIPMAP_LINEAR);
//			gl.glHint(GL2.GL_GENERATE_MIPMAP_HINT, GL2.GL_NICEST);
			LOGGER.info("Initialized OpenGL " + glGetString(GL_VERSION) + " (" + glGetString(GL_VENDOR) + ")");
			try {
				final CLContext context = CLContext.create(configuration.getOpenCLPlatform(), Collections.<CLDevice>singletonList(configuration.getOpenCLDevice()), null, configuration.isShareGlClContext() ? this : null, null);
				LOGGER.info("Initialized " + configuration.getOpenCLPlatform().getInfoString(CL_PLATFORM_VERSION) + " (" + configuration.getOpenCLPlatform().getInfoString(CL_PLATFORM_VENDOR) + ") using device " + configuration.getOpenCLDevice().getInfoString(CL_DEVICE_NAME));
				game.initialize(context, configuration.isShareGlClContext());
				clReleaseContext(context);
			} catch (final LWJGLException e) {
				LOGGER.log(Level.WARNING, "Error initializing OpenCL", e);
			}			 
		}
		
		@Override
		public void keyPressed(final KeyEvent keyEvent) {
			if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE) {
				dispose();
			} else if (keyEvent.getKeyCode() == KeyEvent.VK_LEFT)
				viewerAzimuthSpeed = VIEWER_TURNING_SPEED;
			else if (keyEvent.getKeyCode() == KeyEvent.VK_RIGHT)
				viewerAzimuthSpeed = -VIEWER_TURNING_SPEED;
			else if (keyEvent.getKeyCode() == KeyEvent.VK_UP)
				viewerElevationSpeed = VIEWER_TURNING_SPEED;
			else if (keyEvent.getKeyCode() == KeyEvent.VK_DOWN)
				viewerElevationSpeed = -VIEWER_TURNING_SPEED;
		}

		@Override
		public void keyReleased(final KeyEvent keyEvent) {
			if ((keyEvent.getKeyCode() == KeyEvent.VK_LEFT) || (keyEvent.getKeyCode() == KeyEvent.VK_RIGHT))
				viewerAzimuthSpeed = 0.0f;
			else if ((keyEvent.getKeyCode() == KeyEvent.VK_UP) || (keyEvent.getKeyCode() == KeyEvent.VK_DOWN))
				viewerElevationSpeed = 0.0f;
		}

		@Override
		public void keyTyped(final KeyEvent keyEvent) {}
		
		@Override
		public void mouseWheelMoved(final MouseWheelEvent mouseWheelEvent) {
			viewerDistance -= mouseWheelEvent.getPreciseWheelRotation();
			if (viewerDistance < MIN_VIEWER_DISTANCE)
				viewerDistance = MIN_VIEWER_DISTANCE;
			else if (viewerDistance > MAX_VIEWER_DISTANCE)
				viewerDistance = MAX_VIEWER_DISTANCE;
		}
		
		@Override
		public void render() {
			final long now = System.currentTimeMillis();
			viewerAzimuth += (now - time) / 1000.0f * viewerAzimuthSpeed;
			if (viewerAzimuth < 0.0)
				viewerAzimuth += Math.PI * 2.0;
			else if (viewerAzimuth > Math.PI * 2.0)
				viewerAzimuth -= Math.PI * 2.0;
			viewerElevation += (now - time) / 1000.0f * viewerElevationSpeed;
			if (viewerElevation < 0.0)
				viewerElevation = 0.0;
			else if (viewerElevation > Math.PI / 2.0)
				viewerElevation = Math.PI / 2.0;
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
			glMatrixMode(GL_MODELVIEW);
			glLoadIdentity();
			gluLookAt(Double.valueOf(longitudinalPosition + viewerDistance * Math.cos(viewerElevation) * Math.cos(viewerAzimuth)).floatValue(), Double.valueOf(viewerDistance * Math.sin(viewerElevation)).floatValue(), Double.valueOf(-latitudinalPosition + viewerDistance * Math.cos(viewerElevation) * Math.sin(viewerAzimuth)).floatValue(), longitudinalPosition, 0.0f, -latitudinalPosition, 0.0f, 1.0f, 0.0f);
			game.render();
			time = now;
		}
		
		@Override
		public void windowOpened(final WindowEvent windowEvent) {
			animator.start();
		}

		@Override
		public void windowClosing(final WindowEvent windowEvent) {}

		@Override
		public void windowClosed(final WindowEvent windowEvent) {
			animator.stop();
		}

		@Override
		public void windowIconified(final WindowEvent windowEvent) {
			animator.stop();
		}

		@Override
		public void windowDeiconified(final WindowEvent windowEvent) {
			animator.start();
		}

		@Override
		public void windowActivated(final WindowEvent windowEvent) {
			animator.start();
		}

		@Override
		public void windowDeactivated(final WindowEvent windowEvent) {
			animator.stop();
		}
	}
	private static final long serialVersionUID = 1L;
	private static final int TOP_HEIGHT = 50;
	private static final int TOP_MIN_WIDTH = 800;
	private static final int BOTTOM_HEIGHT = 200;
	private static final int BOTTOM_MIN_WIDTH = 800;
	private static final float VIEWER_TURNING_SPEED = Double.valueOf(Math.PI / 4.0).floatValue();
	private static final float MIN_VIEWER_DISTANCE = 1.0f;
	private static final float MAX_VIEWER_DISTANCE = 1000.0f;
	private static final float FIELD_OF_VIEW = Double.valueOf(Math.toDegrees(Math.PI / 4.0)).floatValue();
	private static final float Z_NEAR = 0.001f;
	private static final float Z_FAR = 1000.0f;
	private static final Logger LOGGER = Logger.getLogger(GuiClient.class.getName());
	
	private static final int LATITUDINAL_SIZE = 3;
	private static final int LONGITUDINAL_SIZE = 3;
	private static final FloatBuffer altitudes = BufferUtils.createFloatBuffer(LATITUDINAL_SIZE * LONGITUDINAL_SIZE);
	private static final Terrain[] terrains = new Terrain[LATITUDINAL_SIZE * LONGITUDINAL_SIZE];
	static {
		for (int i = 0; i < LATITUDINAL_SIZE * LONGITUDINAL_SIZE; i++) {
			altitudes.put(i, i % 2);
			terrains[i] = Terrain.ASH;
		}
	}
	
	private final Configuration configuration;
	private final Game game;
	private final DisplayMode initialDisplayMode;
	private final JPanel top;
	private final GameCanvas canvas;
	private final JPanel bottom;
	private final Timer animator;
	private float latitudinalPosition;
	private float longitudinalPosition;
	private double viewerAzimuth;
	private float viewerAzimuthSpeed;
	private double viewerElevation;
	private float viewerElevationSpeed;
	private float viewerDistance;
	private long time;

	
	
	public static void main(final String[] arguments) {
		try {
			final GuiClient guiClient = new GuiClient(new Configuration());
			guiClient.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
			SwingUtilities.invokeLater(guiClient);
		} catch (final LWJGLException e) {
			LOGGER.log(Level.WARNING, "Error initializing OpenGL", e);
		}
	}

	public GuiClient(final Configuration configuration) throws LWJGLException {
		super(configuration.getScreen().getDefaultConfiguration());
		this.configuration = configuration;
		game = new Game(new Map(LATITUDINAL_SIZE, LONGITUDINAL_SIZE, altitudes, terrains), Collections.<Object>emptySet(), Collections.<Team>emptySet());
		initialDisplayMode = configuration.getScreen().getDisplayMode();
		setTitle(configuration.getResourceBundle().getString("botcraft"));
		setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
		setMinimumSize(new Dimension((TOP_MIN_WIDTH > BOTTOM_MIN_WIDTH) ? TOP_MIN_WIDTH : BOTTOM_MIN_WIDTH, TOP_HEIGHT + BOTTOM_HEIGHT));
		top = new JPanel();
		top.setLayout(new BoxLayout(top, BoxLayout.X_AXIS));
		top.setMinimumSize(new Dimension(TOP_MIN_WIDTH, TOP_HEIGHT));
		top.add(new JLabel("Top 1"));
		top.add(new JLabel("Top 2"));
		top.add(new JLabel("Top 3"));
		add(top);
		canvas = new GameCanvas();
		canvas.setMinimumSize(new Dimension());
		addWindowListener(canvas);
		add(canvas);
		bottom = new JPanel();
		bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));
		bottom.setMinimumSize(new Dimension(BOTTOM_MIN_WIDTH, BOTTOM_HEIGHT));
		bottom.add(new JLabel("Bottom 1"));
		bottom.add(new JLabel("Bottom 2"));
		bottom.add(new JLabel("Bottom 3"));
		add(bottom);
		animator = new Timer(1000 / configuration.getFrameRate(), canvas);
		latitudinalPosition = 0.0f;
		longitudinalPosition = 0.0f;
		viewerAzimuth = 0.0f;
		viewerAzimuthSpeed = 0.0f;
		viewerElevation = 0.0f;
		viewerElevationSpeed = 0.0f;
		viewerDistance = MIN_VIEWER_DISTANCE;
		time = System.currentTimeMillis();

	}
		
	@Override
	public void dispose() {
		super.dispose();
		if (configuration.isDisplayFullScreen())
			configuration.getScreen().setFullScreenWindow(null);
		else
			setVisible(false);
		if (configuration.getScreen().isDisplayChangeSupported())
			configuration.getScreen().setDisplayMode(initialDisplayMode);
		canvas.destroy();
	}		

	@Override
	public void run() {
		if (configuration.getScreen().isDisplayChangeSupported())
			configuration.getScreen().setDisplayMode(configuration.getDisplayMode());
		else
			LOGGER.warning("Changing display mode is not supported");
		final Rectangle bounds = configuration.getScreen().getDefaultConfiguration().getBounds();
		final Dimension size = new Dimension(Double.valueOf(bounds.getWidth()).intValue(), Double.valueOf(bounds.getHeight()).intValue());
		setMaximumSize(size);
		setPreferredSize(size);
		setSize(size);
		final Dimension topSize = new Dimension(Double.valueOf(bounds.getWidth()).intValue(), TOP_HEIGHT);
		top.setMaximumSize(topSize);
		top.setPreferredSize(topSize);
		top.setSize(topSize);
		final Dimension canvasSize = new Dimension(Double.valueOf(bounds.getWidth()).intValue(), Double.valueOf(bounds.getHeight()).intValue() - TOP_HEIGHT - BOTTOM_HEIGHT);
		canvas.setMaximumSize(canvasSize);
		canvas.setPreferredSize(canvasSize);
		canvas.setSize(canvasSize);
		final Dimension bottomSize = new Dimension(Double.valueOf(bounds.getWidth()).intValue(), BOTTOM_HEIGHT);
		bottom.setMaximumSize(bottomSize);
		bottom.setPreferredSize(bottomSize);
		bottom.setSize(bottomSize);
		pack();
		if (configuration.isDisplayFullScreen())
			configuration.getScreen().setFullScreenWindow(this);
		else
			setVisible(true);
		canvas.requestFocusInWindow();
	}	
}
